<template>
  <div ref="sliderBody" class="slider-main">
    <div class="slider-body">
      <div
        ref="sliderMenu"
        class="slider-menu"
        :style="{ transform: 'translateY(' + top + 'px)' }"
      >
        <div
          class="slider-menu-item"
          v-for="(item, index) in data"
          :key="index"
          :style="item.style || {}"
          :class="{ 'slider-menu-item-active': current === index }"
          @click="scrollToView(index)"
        >
          <span>{{ item.name }}</span>
        </div>
      </div>

      <div ref="sliderInfo" class="slider-info">
        <div
          ref="scrollItem"
          class="component-info"
          v-for="(item, index) in data"
          :key="index + '-cmp'"
          :style="{
            minHeight: (index === data.length - 1 ? menuHeight : 0) + 'px',
          }"
        >
          {{ index }}
          <component :is="item.component"></component>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: "SliderMenu",
  props: {
    data: {
      type: [String, Number, Boolean, Object, Array],
      default: () => [
        { name: "技术能力", style: {}, component: {} },
        { name: "语音技术", style: {}, component: {} },
        { name: "文字识别", style: {}, component: {} },
        { name: "人脸与人体识别", style: {}, component: {} },
        { name: "视频技术", style: {}, component: {} },
        { name: "AR与VR", style: {}, component: {} },
        { name: "数据智能", style: {}, component: {} },
        { name: "AR与VR", style: {}, component: {} },
        { name: "数据智能", style: {}, component: {} },
        { name: "AR与VR", style: {}, component: {} },
        { name: "数据智能", style: {}, component: {} },
        { name: "AR与VR", style: {}, component: {} },
        { name: "数据智能", style: {}, component: {} },
      ],
    },
  },
  data() {
    return {
      current: 0,
      bodyOffset: 10,
      topOffset: 10,
      oneMenuHeight: 39,
      minViewMenuCount: 3,
      windowViewHeight: this.getWindowHeight(),
      menuHeight: 0,
    };
  },
  computed: {
    top() {
      if (this.topOffset >= this.bodyOffset) {
        return this.bodyOffset;
      }
      return this.topOffset;
    },
    currentSliderHeight() {
      // 当前滑动的菜单高度
      return (this.current + 1) * this.oneMenuHeight;
    },
    minViewMenuHeight() {
      // 菜单最小可见高度
      return this.minViewMenuCount * this.oneMenuHeight;
    },
    viewHeight() {
      // 可视图中，最大能显示的可见高度
      return this.windowViewHeight - this.bodyOffset;
    },
  },
  methods: {
    scrollToView(index) {
      // 滑动到指定组件位置
      let data = this.getScrollItems();
      if (index >= 0 && index < data.length) {
        let top = this.getNodeTop(data[index]) - this.bodyOffset;
        window.scrollTo({ top: top < 0 ? 0 : top, behavior: "smooth" });
      }
    },
    handleScroll() {
      let data = this.getScrollItems();
      this.windowViewHeight = this.getWindowHeight();
      const scrollTop = this.getScroll(window, true);
      let baseTop = scrollTop + this.bodyOffset; // 滑动高度 + 起始位置
      let maxTop = -99999;
      let maxVisibleBottom =
        this.getMenuTop() + this.currentSliderHeight + this.minViewMenuHeight; // 当前最大能显示的菜单所在位置
      // 小于最小显示数量 || 菜单完全显示在可视视图中 || 当前选中的菜单 + 最小展示的菜单 在视图中
      if (
        this.current < this.minViewMenuCount ||
        this.menuHeight + this.bodyOffset < this.windowViewHeight ||
        maxVisibleBottom < this.windowViewHeight
      ) {
        this.topOffset = this.bodyOffset;
      }
      let oldCurrent = this.current;
      data.forEach((target, index) => {
        const elOffset = this.getOffset(target);
        let realTop = this.getNodeTop(target) - baseTop;
        if (realTop <= 0 && maxTop <= realTop) {
          maxTop = realTop;
          this.current = index;
        }
      });
      let minMenuOffset = this.menuHeight - this.viewHeight;
      let minTopOffset = this.bodyOffset - minMenuOffset; // 菜单完全显示到底时，最大偏移量
      let bottom = this.getInfoBottom() - baseTop - this.viewHeight; // 内容显示到底时：bottom=0
      if (this.current >= data.length - 1 && bottom < 0) {
        let lastNodeHeight =
          (this.getOffset(data[this.current]) || {}).height || 0;
        let minHeight = Math.min(this.menuHeight, this.viewHeight); // 判断菜单 和可见视图 哪个小
        // 在 minHeight < lastNodeHeight 时：必定能完全显示最后一个组件页面
        // 他们的差值：代表 （组件页面 + 多少）能与菜单对齐，或者与视图窗口对齐
        let bottomOffset = minHeight - lastNodeHeight;
        this.topOffset =
          minTopOffset + bottom + (bottomOffset > 0 ? bottomOffset : 0);
      } else if (
        oldCurrent != this.current &&
        this.current >= this.minViewMenuCount &&
        this.windowViewHeight < maxVisibleBottom
      ) {
        let moveHeight =
          (oldCurrent > this.current ? 1 : -1) * this.oneMenuHeight;
        this.topOffset =
          (this.topOffset < minTopOffset ? minTopOffset : this.topOffset) +
          moveHeight;
      }
    },
    getWindowHeight() {
      return window.innerHeight || document.documentElement.clientHeight;
    },
    getNodeBottom(node) {
      const currentOffset = this.getOffset(node) || {};
      return (currentOffset.top || 0) + (currentOffset.height || 0);
    },
    getNodeTop(node) {
      const currentOffset = this.getOffset(node) || {};
      return currentOffset.top || 0;
    },
    getMenuTop() {
      if (this.$refs.sliderMenu) {
        return this.getNodeTop(this.$refs.sliderMenu);
      }
      return 0;
    },
    getMenuBottom() {
      if (this.$refs.sliderMenu) {
        return this.getNodeBottom(this.$refs.sliderMenu);
      }
      return 0;
    },
    getInfoBottom() {
      if (this.$refs.sliderInfo) {
        return this.getNodeBottom(this.$refs.sliderInfo);
      }
      return 0;
    },
    getScrollItems() {
      let refs = [];
      if (this.$refs.scrollItem && this.$refs.scrollItem.style) {
        refs.push(this.$refs.scrollItem);
      } else if (this.$refs.scrollItem && this.$refs.scrollItem.length) {
        refs = this.$refs.scrollItem;
      }
      return refs;
    },
    getScroll(target, top) {
      const prop = top ? "pageYOffset" : "pageXOffset";
      const method = top ? "scrollTop" : "scrollLeft";
      let ret = target[prop];
      if (typeof ret !== "number") {
        ret = window.document.documentElement[method];
      }
      return ret;
    },
    getOffset(element) {
      const rect = element.getBoundingClientRect();
      const scrollTop = this.getScroll(window, true);
      const scrollLeft = this.getScroll(window);

      const docEl = window.document.body;
      const clientTop = docEl.clientTop || 0;
      const clientLeft = docEl.clientLeft || 0;
      let height = rect.bottom - rect.top;
      if (height === 0 && element.parentNode) {
        let parentRect = element.parentNode.getBoundingClientRect();
        height = parentRect ? parentRect.height || 0 : 0;
      }
      return {
        top: rect.top + scrollTop - clientTop,
        left: rect.left + scrollLeft - clientLeft,
        height: height,
        width: rect.right - rect.left,
      };
    },
  },
  mounted() {
    this.bodyOffset = this.$refs.sliderBody.offsetTop;
    this.menuHeight = (this.getOffset(this.$refs.sliderMenu) || {}).height || 0;
    this.topOffset = this.bodyOffset;
    window.addEventListener("scroll", this.handleScroll, false);
    window.addEventListener("resize", this.handleScroll, false);
    this.handleScroll();
  },
  beforeDestroy() {
    window.removeEventListener("scroll", this.handleScroll, false);
    window.removeEventListener("resize", this.handleScroll, false);
  },
};
</script>
<style scoped>
.slider-main {
  display: flex;
  flex-direction: column;
}
.slider-body {
  display: flex;
  flex-wrap: wrap;
}
.slider-menu {
  position: fixed;
  left: auto;
  width: 180px;
  overflow: auto;
  color: #000;
  height: auto;
  top: 0px;
  transition: transform 0.1s linear;
}
.slider-menu-item {
  border-left: 2px solid #e8eaed;
  display: block;
  padding: 15px 0 0 30px;
  position: relative;
  line-height: 24px;
  color: inherit;
  cursor: pointer;
}
.slider-menu-item-active,
.slider-menu-item:hover {
  color: #1a73e8;
}
.slider-menu-item-active {
  border-left: 2px solid #1a73e8;
}
.component-info {
  height: 100px;
  width: 600px;
  border: 1px solid #f0f0f0;
}
.slider-info {
  margin-left: 200px;
  display: flex;
  flex-direction: column;
  position: relative;
}
.menu-move-animation {
  width: 100%;
  padding: 0px;
  animation-duration: 0.3s;
  animation-fill-mode: both;
  animation-name: fadeInLeft;
}
@keyframes fadeInLeft {
  from {
    opacity: 0;
    /* transform: translate3d(100%, 0, 0); */
  }
  to {
    opacity: 1;
    transform: none;
  }
}
</style>
